package wortubung.lang.de;

import static wortubung.lang.LanguageManager.*;
import static wortubung.lang.de.DeutschSpracheWerkzeug.*;
import static wortubung.util.LatinScript.*;
import static wortubung.util.CommonUtils.*;

import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;

import wortubung.core.GENDER;
import wortubung.core.POS;
import wortubung.core.POSInfo;
import wortubung.inflection.DeclensionResult;
import wortubung.inflection.Decliner;
import wortubung.inflection.UnknownDeclensionException;

public class NomenDeklinationRegel implements Decliner {

	public static String[] resultTitles = { "Single", "Plural" };

	public static DeclensionResult decline(String noun, POSInfo posInfo) {
		Decliner rule;
		GENDER gender;

		// If posInfo knows how to decline, do it:
		if (posInfo != null && posInfo.getDecliner() != null)
			return posInfo.getDecliner().decline(noun);

		// If this noun is known, use the known declension:
		rule = wortDeklinationen.get(noun);
		if (rule != null) {
			if (((NomenDeklinationRegel)rule).isRegularNounEnding()) {
				gender = ((NomenDeklinationRegel)rule).getGender();
				rule = findRuleForEnding(noun, gender);
				if (rule != null)
					return rule.decline(noun);
				return guessNounDeclension(noun, gender);
			}
			return rule.decline(noun);
		}

		// Now, the best hope is that the noun has a regular ending:
		gender = (posInfo != null) ? posInfo.getGender() : null;
		rule = findRuleForEnding(noun, gender);
		if (rule != null)
			return rule.decline(noun);

		// Finally, if we know the gender, we may try to guess:
		if (gender != null)
			return guessNounDeclension(noun, gender);

		// Well, now we are truly out of luck
		UnknownDeclensionException.badDeclension(noun);
		return null;
	}

	private static DeclensionResult guessNounDeclension(String noun, GENDER gender) {
		final String[] nonEVows = { "a", "i", "o", "u" };
		final String[] pluralSufficesWithER = { "nn", "nd", "ch" };

		// PART I. Guess plural
		String plural;
		
		// 1. Feminines usu. add -en
		if (gender == GENDER.f) {
			plural = appendEN(noun);
		}
		else if (noun.endsWith("er")){
			// 3. Masculine and neuter ending with -er: without change; may add umlaut.
			plural = addFirstUmlaut(noun);
		}
		else if (endsWith(noun, nonEVows)) {
			// 4. Nouns ending with a vow but not -e, and foreign words, add -s.
			plural = noun + 's';
		}
		else if (noun.endsWith("e")) {
			plural = appendEN(noun);
		}
		else {
			// 2. Masculine and neuter usu. add -e or -er; masc. usu. add umlaut; neuter never.
			plural = noun;
			if (gender == GENDER.m)
				plural = addFirstUmlaut(plural);
			plural += (endsWith(noun, pluralSufficesWithER) ? "er" : "e");
		}
		
		// PART II. Guess single genitive and others
		String singGen;

		// 1. Feminine: no change
		if (gender == GENDER.f) {
			singGen = noun;
		}
		else {
			// 2. Masc., neuter: single-syllable: add -(e)s; multi-syllable: add -s
			singGen = noun + (isSingleSyllable(noun) ? "(e)s" : "s"); 

			// 3. Dative of masc. neuter: optionally add -e; we ignore this.
		}

		String[] nom = new String[]{ noun,    plural };
		String[] gen = new String[]{ singGen, plural };
		String[] dat = new String[]{ noun,    appendMissingLastN(plural) };
		String[] akk = nom;
		return new DeclensionResult(gender, nom, gen, dat, akk, resultTitles, true);
	}

	//////////////////////////////////////////
	// Individual rule

	private GENDER  gender;
	private boolean genitiveOnly;
	private String  singleSuffix;

	private String  pluralSuffix;
	private int     pluralReplaceDigits = 0;
	private boolean pluralUmlaut;

	private NomenDeklinationRegel(GENDER gender, boolean genitiveOnly, String singleSuffix, String pluralSuffix, int pluralReplaceDigits, boolean umlaut) {
		this.gender       = gender;
		this.genitiveOnly = genitiveOnly;
		this.singleSuffix = singleSuffix;
		this.pluralSuffix = pluralSuffix;
		this.pluralReplaceDigits = pluralReplaceDigits;
		this.pluralUmlaut = umlaut;
	}

	@Override
	public DeclensionResult decline(String noun) {
		String[] nom;
		String[] gen;
		String[] dat;
		String[] akk;

		if (isN()) { // all declensions add (e)n
			String declined = noun.endsWith("e") ? (noun + 'n') : (noun + "en");
			nom = new String[]{ noun,     declined };
			gen = new String[]{ declined, declined };
			dat = new String[]{ declined, declined };
			akk = new String[]{ declined, declined };
		}
		else {
			String plural = declinePlural(noun);
			nom = toArray(noun, plural);
			if (length(getSingleSuffix()) == 0) {
				gen = toArray(noun, plural);
				dat = toArray(noun, plural);
				akk = toArray(noun, plural);
			}
			else {
				String sing = noun + getSingleSuffix();
				if (isGenitiveOnly()) {
					gen = toArray(sing, plural);
					dat = toArray(noun, plural);
					akk = toArray(noun, plural);
				} else {
					gen = toArray(sing, plural);
					dat = toArray(sing, plural);
					akk = toArray(sing, plural);
				}
			}
			if (dat.length > 1 && !dat[1].endsWith("n"))
				dat[1] = appendMissingLastN(dat[1]);
		}

		return new DeclensionResult(getGender(), nom, gen, dat, akk, resultTitles, false);
	}

	private String declinePlural(String noun) {
		if (hasNoPlural())
			return null;
		if (getPluralReplaceDigits() > 0)
			noun = chopRight(noun, getPluralReplaceDigits());
		if (isPluralUmlaut())
			noun = addFirstUmlaut(noun);
		if (length(getPluralSuffix()) > 0)
			return noun + getPluralSuffix();
		return noun;
	}

	private String[] toArray(String s1, String s2) {
		return (s2 == null) ? new String[]{ s1 } : new String[]{ s1, s2 };
	}

	GENDER  getGender() { return gender; }
	
	boolean isN() { return !genitiveOnly && "N".equals(singleSuffix); }
	boolean isRegularNounEnding() { return "?".equals(singleSuffix); }

	boolean isGenitiveOnly() { return genitiveOnly; }
	String  getSingleSuffix() { return singleSuffix; }

	boolean hasNoPlural() { return "-".equals(pluralSuffix); }
	String  getPluralSuffix() { return pluralSuffix; }
	int     getPluralReplaceDigits() { return pluralReplaceDigits; }
	boolean isPluralUmlaut() { return pluralUmlaut; }

	public static void addDeklination(String noun, NomenDeklinationRegel regel) {
		wortDeklinationen.put(noun, regel);
	}

	// c.f. Nomina.de.txt
	public static NomenDeklinationRegel parse(String line) {
		int idx = line.indexOf(' ');
		String posStr = idx > 0 ? line.substring(0, idx).trim() : line;
		if (idx > 0)
			line = line.substring(idx+1).trim();
		else
			line = null;

		POS pos = null;
		if (posStr.endsWith("m"))
			pos = POS.NOUN_M;
		else if (posStr.endsWith("f"))
			pos = POS.NOUN_F;
		else if (posStr.endsWith("n"))
			pos = POS.NOUN;
		else
			UnknownDeclensionException.badDeclension("Unknown POS: " + posStr + ".");
//			UnknownDeclensionException.badDeclension("Ungültige Regel für Geschlechter.");

		if (isEmpty(line)) {
			switch(pos) {
			case NOUN:   return regularEndingN;
			case NOUN_M: return regularEndingM;
			case NOUN_F: return regularEndingF;
			}
		}

		return parse(null, pos, line);
	}

	public static NomenDeklinationRegel parse(String noun, POS nounPOS, String line) {
		String[] sa = line.split(" +");
		NomenDeklinationRegel ret = null;
		try {
			if (sa[0].equalsIgnoreCase("*n") && sa.length == 1)
				return nDeclension;

			if (sa.length < 2)
				UnknownDeclensionException.badDeclension("Ungültige Regel für Deklination: '" + line + "'.");
			if (sa[0].endsWith(","))
				sa[0] = chopRight(sa[0], 1);

			GENDER gender = GENDER.fromPOS(nounPOS);
			String sig = gender.name() + sa[0] + sa[1];
			ret = nomenDeklinationRegls.get(sig);
			if (ret != null)
				return ret;

			boolean umlaut = sa[1].startsWith(":");
			String pluralSuffix = umlaut ? sa[1].substring(1) : sa[1];
			int pluralReplaceDigits = 0;
			if (pluralSuffix.startsWith("-"))
				pluralSuffix = pluralSuffix.substring(1);
			else if (pluralSuffix.startsWith("/")) {
				String[] tmp = pluralSuffix.split("/");
				pluralReplaceDigits = tmp[0].trim().length();
				pluralSuffix = tmp[1].trim();
			}
			char ch = sa[0].charAt(0);
			if (ch != '-' && ch != '*')
				UnknownDeclensionException.badDeclension("Ungültige Regel für single Deklination: '" + line + "'.");

			ret = new NomenDeklinationRegel(
					gender,
					ch == '-',                    // genitive only
					enAccent(sa[0].substring(1)), // single suffix
					enAccent(pluralSuffix),
					pluralReplaceDigits,
					umlaut);
			nomenDeklinationRegls.put(sig, ret);
			return ret;
		} catch(Exception e) {
			String msg = (e instanceof UnknownDeclensionException) ? e.getMessage() : "Format.";
			UnknownDeclensionException.badDeclension("Ungültige Substantivdeklination Regel: '" + line + "': " + msg);
		} finally {
			if (ret != null && noun != null)
				wortDeklinationen.put(noun, ret);
		}
		return null;
	}

	//////////////////////////////////////////
	// Rule repositories

	private static final HashMap<String,NomenDeklinationRegel> wortDeklinationen = // key: noun
			new HashMap<String,NomenDeklinationRegel>();

	private static final NomenDeklinationRegel nDeclension =
			new NomenDeklinationRegel(GENDER.m, false, "N", null, 0, false);
	private static final NomenDeklinationRegel regularEndingM =
			new NomenDeklinationRegel(GENDER.m, false, "?", null, 0, false);
	private static final NomenDeklinationRegel regularEndingF =
			new NomenDeklinationRegel(GENDER.f, false, "?", null, 0, false);
	private static final NomenDeklinationRegel regularEndingN =
			new NomenDeklinationRegel(GENDER.n, false, "?", null, 0, false);
	private static final HashMap<String,NomenDeklinationRegel> nomenDeklinationRegls = // key: signature
			new HashMap<String,NomenDeklinationRegel>();
	private static final HashMap<String, RegularNounEnding[]> regularNounEndings =     // key: suffix last2
			new HashMap<String, RegularNounEnding[]>();

	//////////////////////////////////////////////////////
	// Regular noun ending

	static class RegularNounEnding {
		private String ending;
		private boolean isGenderRequired;
		private NomenDeklinationRegel rule;

		private RegularNounEnding(String ending, NomenDeklinationRegel rule, boolean isGenderRequired) {
			this.ending = ending;
			this.isGenderRequired = isGenderRequired;
			this.rule = rule;
		}

		public NomenDeklinationRegel getRule() { return rule; }

		public boolean fitsEnding(String noun) { return noun != null && noun.endsWith(ending); }
		public boolean fitsGender(GENDER g) { return g != null &&  rule.getGender() == g; }
		public boolean isGenderRequired() { return isGenderRequired; }
	}

	private static NomenDeklinationRegel findRuleForEnding(String noun, GENDER gender) {
		RegularNounEnding[] rules = regularNounEndings.get(last(noun, 2));
		if (rules != null) {
			for (RegularNounEnding rne : rules) {
				if (!rne.fitsEnding(noun))
					continue;
				if (gender == null) {
					if (!rne.isGenderRequired())
						return rne.getRule();
				}
				else if (rne.fitsGender(gender)) {
					return rne.getRule();
				}
			}
		}
		return null;
	}

	private static void addRegularNounEndings(String ending, NomenDeklinationRegel rule, boolean isGenderRequired) {
		String last2 = last(ending, 2);
		RegularNounEnding rne = new RegularNounEnding(ending, rule, isGenderRequired);

		RegularNounEnding[] tmp = regularNounEndings.get(last2);
		if (tmp == null) {
			regularNounEndings.put(last2, new RegularNounEnding[] { rne });
			return;
		}

		RegularNounEnding[] a = new RegularNounEnding[tmp.length + 1];
		a[tmp.length] = rne;
		for (int i = tmp.length - 1; i >= 0; --i)
			a[i] = tmp[i];
		Arrays.sort(a, new Comparator<RegularNounEnding>() {
			@Override public int compare(RegularNounEnding r1, RegularNounEnding r2) {
				return new Integer(r2.ending.length()).compareTo(r1.ending.length());
			}
		});
		regularNounEndings.put(last2, a);
	}

	//////////////////////////////////////////////////////
	// Read data

	static {
		try {
			String[] lines = readFileFromCPAsString(DE_NOMINA_FILE).split("\n");
			for (String line : lines) {
				line = line.trim();
				if (line.length() == 0 || line.startsWith("#"))
					continue;
				int idx1 = line.indexOf('[');
				int idx2 = line.indexOf(']');
				if (idx1 < 0 || idx2 <= idx1)
					throw new RuntimeException("Datei '" + DE_NOMINA_FILE + "' hat Syntax-Fehler in Zeile '" + line + "'.");

				NomenDeklinationRegel regle = parse(line.substring(idx1+1, idx2).trim());
				String word = line.substring(0, idx1).trim();
				if (word.startsWith("~"))
					addRegularNounEndings(enAccent(word.substring(1)), regle, false);
				else if (word.startsWith("+"))
					addRegularNounEndings(enAccent(word.substring(1)), regle, true);
				else
					addDeklination(enAccent(word), regle);
			}
		} catch(Exception e) {
			e.printStackTrace();
		}
	}
}
